This month we are talking a look at how to handle money without getting bit by errors in floating point values...
Liberty Basic has two types of variables: Numeric and String. String variable names are always suffixed with a "$" which is a string indicator. Variables that do not have the string indicator are assumed to be numeric. Putting string data into numeric variables will produce a Type Mismatch error. Liberty Basic permits numbers to be put into strings, but you can not perform math functions with the string variables.
There is a subclass of numeric variables that Liberty Basic uses, but isolates the user from. These are floating point and integer numeric variables. Liberty Basic supports both forms using the numeric variable. The translation is mostly transparent to the user. Numbers that have no decimal component are integers and numbers with a decimal component are floating point values.
You must be wondering why even bring this up. The issue is in the way modern computers (not Liberty Basic) approximate a floating point value. You read it right. The values are approximated, not precise numeric values. We get fooled when working with small values and integer numbers and we think that everything is exactly the way we entered the data, but floating point values have a small (very small) degree of error inherent in them. This error can be exasperated when performing some calculations, and can result in difficulty evaluating seemingly equal values as equal. Take for example:
v1=0.55
v2=208.55
v3 = v2 - 208
'see if they are equal...
if v1 = v3 then
print "The values are exactly equal"
else
print "The values are NOT exactly equal"
end if
print "v1 = ";v1;", v3 = ";v3
It seems hard to believe that the two seemingly equal values could not be equal. What is going on? Remember these values are approximations. Try adding these two lines to the end of the program above:
print using(".###################################################",v1)
print using(".###################################################",v3)
Now you will see that these values are indeed not equal. Usually this does not cause problems, but consider the case where you are handling money. Using approximations will result in loss of integrity of the penny amounts, possibly resulting in larger losses over time if you do not compensate for it. But what can be done - we already said all decimal values are floating point and all floating point values are approximations and prone to error.
Money is interesting if you think about it. You are usually talking about a fix level of decimal places. Somewhere between two and four. More than four does not make much sense to common folks really. Here in lies the answer to the problem. You can do simple money math by assuming the decimal places and working with just integers. Add the decimal places back in when you display the values only. The computer does not really care as long as you are consistent.
If you decide you want to always carry 3 decimal places, then do that everywhere. If the user enters $1, then you must convert this to 1000 units of one tenth of a cent. Other conversions may look like this:
2.345 = 2345
5.1 = 5100
2.112233 = 21122
There are two caveats - Multiplication of these values with assumed decimal places will double the decimal places. You will need to adjust the value so that it is compliant with the assumed decimal (by hacking off some trailing values). The other is division which almost always results in a decimal value and thus a floating point value with some loss. In this case you must convert the number value back to an integer as soon as the division is complete by multiplying by the assumed decimal value (1000 in our example above) and then truncating the trailing decimal values using one of the special functions shown below.
Liberty Basic will attempt to store numeric values in integer format when ever possible. That means that if a decimal number can be changed into an integer through an arithmetic operation, it will be stores as an integer. You can do this by taking a number like 123.45 and multiplying by 100. Try this:
a = 123.45
b = 12345
a = a*100
if a = b then print "equal"
The problem comes in when the results of the arithmetic operations are still in decimal form. Then they are still floats and can produce non equal values that look equal. This is most common when dividing. Here is a simple example:
a = 123.4555
b = 12345.55
b = b/100
print a, b
if a = b then print "equal"
if a <> b then print "NOT equal"
The situation with division (and sometimes multiplication) accuracy can be an issue when trying to build conversion routines that force the values to be integers. The INT function is usually quite reliable and could have been successfully employed to create a set of conversion routines, but I wanted rock solid converters. To do this I fell back on string manipulation of the numeric values. Treating them as strings allows us to work with decimal values and retain their true value when going back and forth between the displayed values (with decimals) and the stored values (with assumed decimal places).
There are two functions. strToCur() which will take a string value with a decimal place and convert it to an integer value with an assumed decimal. You specify the decimal places you are assuming in the call. Zero to six decimal places are valid. The other function is curToStr$() which will take an integer value with an assumed decimal and create a string value with that exact human readable number including the decimal. Decimal place is once again pass as part of the call and is a value from 0 to 6.
Both functions have a third parameter that is the status of the call. It is passed into the function by reference so that the function can alter the variable's value to indicate success of failure of the call. When successful the status will contain a zero.
Here are the routines:
function curToStr$(inVal, decimals, byref status)
'Use this function to convert a numeric currency with an assumed
' decimal place to a string value for printing or display.
' decimal is the number of fixed decimals that are assumed in
' the number.
'Call status (success or failure) is passed back in the variable
' status which is passed by refrence so that the function can change
' the value. (A one indicates failure)
status = 0
if decimals < 0 or decimals > 6 then decimals = 0
dec$ = "00000000000"
'this function requires very little error checking. We must be getting
' numeric values or a Type Mismatch will occur. The only thing we don't
'permit is a decimal
a$ = str$(inVal)
if instr(a$,".") > 0 then
'the numeric value is invalid - exit
curToStr$ = ""
status = 1
exit function
end if
if decimals > 0 then
'place the decimal point
a = len(a$)
'we may need to pad with zeros if string is shorter than decimal range
if a < decimals+1 then
a$ = left$(dec$, decimals-a+1) + a$
'the length of a changed - get the new length
a = len(a$)
end if
b$ = left$(a$,a-decimals)
c$ = right$(a$,decimals)
newVal$ = b$ + "." + c$
else
newVal$ = a$
end if
curToStr$ = newVal$
end function
function strToCur(inVal$, decimals, byref status)
'Use this function to convert a string currency value to a
' numeric value. The decimal is assumed at the number of
' places indicated by "decimals"
'Call status (success or failure) is passed back in the variable
' status which is passed by refrence so that the function can change
' the value. (A one indicates failure)
'do some simple error testing
er = 0
a = len(inVal$)
if decimals < 0 or decimals > 6 then decimals = 0
dec$ = "0000000"
dec$ = left$(dec$,decimals)
if a = 0 then
er = 1
else
cnt = 0
for x = 1 to a
if mid$(inVal$,x,1) = "." then cnt = cnt + 1
if instr("1234567890.-",mid$(inVal$,x,1),1) = 0 then
er = 1
exit for
end if
next x
if cnt > 1 then er = 1
end if
if er = 0 then
i = instr(inVal$,".")
if i > 0 then
b$ = left$(inVal$,i-1)
c$ = right$(inVal$,a-i)
c$ = left$(c$+dec$,decimals)
newVal$ = b$ + c$
else
newVal$ = inVal$ + dec$
end if
end if
status = er
strToCur = val(newVal$)
end function
If you would like to exercise the functions, I have a simple program that will test it in action. Add this to the two routines and run the program in Liberty Basic.
dat$ = "234.56 3 1111 0 12345.5 2 22.33.44 2 8765.32 2 432.34322 3 342ej.2 2 -234.1 1 trdfs 2 0.00 3 0 2"
for x = 1 to 22 step 2
a$ = word$(dat$, x)
dec = val(word$(dat$, x+1))
b = strToCur(a$, dec, status)
print "Original = ";a$
print "Decimals = ";dec
print "Conversion = ";b
print "Status of call: ";status
print "..."
a$ = curToStr$(b, dec, status)
print "ReConversion = ";a$
print "Status of call: ";status
print "========================="
next x